package net.avcompris.jaxen.yaml;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;
import static net.avcompris.jaxen.yaml.YamlNodes.isEmptyAttributeValue;
import static net.avcompris.jaxen.yaml.YamlNodes.isSimpleValue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.NamespaceContext;

import net.avcompris.binding.BindConfiguration;
import net.avcompris.logging.AvcLog;

import org.apache.commons.logging.LogFactory;
import org.jaxen.FunctionCallException;
import org.jaxen.Navigator;
import org.jaxen.UnsupportedAxisException;
import org.jaxen.XPath;
import org.jaxen.saxpath.SAXPathException;

import com.avcompris.common.annotation.Nullable;
import com.avcompris.lang.NotImplementedException;
import com.avcompris.util.YamlUtils;
import com.google.common.collect.Iterators;

final class YamlDocumentNavigator implements Navigator {

	public YamlDocumentNavigator(final Object rootnode,
			@Nullable final NamespaceContext nsContext,
			final BindConfiguration configuration) {

		this.rootNode = nonNullArgument(rootnode, "rootnode");
		this.nsContext = nsContext;
		this.configuration = nonNullArgument(configuration, "configuration");
	}

	private final Object rootNode;

	private final NamespaceContext nsContext;

	private final BindConfiguration configuration;

	/**
	 * for, er... serialization?!#WTF
	 */
	private static final long serialVersionUID = 2692504401242871808L;

//	private static final SimpleLog log0 = new SimpleLog(
	//		YamlDocumentNavigator.class.getSimpleName()); // TODO trash
	//{
		//log0.setLevel(SimpleLog.LOG_LEVEL_DEBUG);
	//}
	private static final AvcLog log = new AvcLog(//log0); // TODO SimpleLog
 LogFactory.getLog(YamlDocumentNavigator.class));

	/**
	 * return <tt>null</tt>.
	 */
	@Override
	public String getElementNamespaceUri(final Object element) {

		if (log.isDebugEnabled()) {
			log.debugf("getElementNamespaceUri(", element, ")");
		}

		nonNullArgument(element, "element");

		final String nsUri;

		if (element instanceof YamlNode) {

			nsUri = ((YamlNode) element).getNamespaceUri();

		} else {

			nsUri = null;
		}

		if (log.isDebugEnabled()) {
			log.debugf("  => getElementNamespaceUri(): ", nsUri);
		}

		return nsUri;
	}

	@Override
	public String getElementName(@Nullable final Object element) {

		if (log.isDebugEnabled()) {
			log.debugf("getElementName(", element, ")");
		}

		final String name;

		if (element == null) {

			name = null;

		} else if (YamlNode.class.isInstance(element)) {

			name = ((YamlNode) element).getName();

		} else if (Map.class.isInstance(element)
				&& ((Map<?, ?>) element).size() == 1) {

			final Object key = ((Map<?, ?>) element).keySet().iterator().next();

			if (String.class.isInstance(key)) {

				name = (String) key;

			} else {

				throw new NotImplementedException("key: " + key);
			}

		} else {

			throw new NotImplementedException("element: " + element);
		}

		if (log.isDebugEnabled()) {
			log.debugf("  => getElementName(): ", name);
		}

		return name;
	}

	@Override
	public String getElementQName(@Nullable final Object element) {

		return getElementName(element);
	}

	/**
	 * return <tt>null</tt>.
	 */
	@Override
	public String getAttributeNamespaceUri(@Nullable final Object attr) {

		if (log.isDebugEnabled()) {
			log.debugf("getAttributNamespaceUri(", attr, ")");
		}

		return null;
	}

	@Override
	public String getAttributeName(final Object attr) {

		if (log.isDebugEnabled()) {
			log.debugf("getAttributeName(", attr, ")");
		}

		nonNullArgument(attr, "attr");

		if (YamlNode.class.isInstance(attr)) {

			return ((YamlNode) attr).getName();
		}

		throw new NotImplementedException();
	}

	@Override
	public String getAttributeQName(final Object attr) {

		return getAttributeName(attr);
	}

	@Override
	public boolean isDocument(@Nullable final Object object) {

		if (log.isDebugEnabled()) {
			log.debugf("isDocument(", object, ")");
		}

		if (object == null) {

			return false;
		}

		// return object == rootNode;

		// log("isDocument(): " + object.getClass());

		throw new NotImplementedException();
	}

	@Override
	public boolean isElement(@Nullable final Object node) {

		if (log.isDebugEnabled()) {
			log.debugf("isElement(", node, ")");
		}

		final boolean element;

		if (node == null) {

			element = false;

		} else {

			// log("isElement(): " + object.getClass());

			if (YamlNode.class.isInstance(node)) {

				if (configuration.isNodesElementsEverywhere()) {

					element = true;

				} else {

					final Object content = ((YamlNode) node).getContent();

					element = content != null && !isSimpleValue(content);
				}

			} else if (Map.class.isInstance(node)) {

				if (((Map<?, ?>) node).size() == 1) {

					element = true;

				} else {

					element = false;
				}

			} else {

				element = false;
			}
		}

		if (log.isDebugEnabled()) {
			log.debugf("  => isElement(): ", element);
		}

		return element;
	}

	@Override
	public boolean isAttribute(@Nullable final Object object) {

		if (log.isDebugEnabled()) {
			log.debugf("isAttribute(", object, ")");
		}

		if (object == null) {

			return false;
		}

		if (YamlNode.class.isInstance(object)) {

			if (configuration.isNodesElementsEverywhere()) {

				return false;
			}

			final Object content = ((YamlNode) object).getContent();

			return isEmptyAttributeValue(content) || isSimpleValue(content);
		}

		throw new NotImplementedException();
	}

	/**
	 * return <tt>false</tt>.
	 */
	@Override
	public boolean isNamespace(@Nullable final Object object) {

		if (log.isDebugEnabled()) {
			log.debugf("isNamespace(", object, ")");
		}

		if (object == null) {

			return false;
		}

		// log("isNamespace(): " + object.getClass());

		return false;
	}

	/**
	 * return <tt>false</tt>.
	 */
	@Override
	public boolean isComment(@Nullable final Object object) {

		return false;
	}

	@Override
	public boolean isText(@Nullable final Object object) {

		if (log.isDebugEnabled()) {
			log.debugf("isText(", object, ")");
		}

		if (object == null) {

			return false;
		}

		return isSimpleValue(object);
	}

	/**
	 * return <tt>false</tt>.
	 */
	@Override
	public boolean isProcessingInstruction(@Nullable final Object object) {

		return false;
	}

	/**
	 * return <tt>null</tt>.
	 */
	@Override
	public String getCommentStringValue(@Nullable final Object comment) {

		return null;
	}

	@Override
	public String getElementStringValue(@Nullable final Object element) {

		if (log.isDebugEnabled()) {
			log.debugf("getElementStringValue(", element, ")");
		}

		if (YamlNode.class.isInstance(element)) {

			return ((YamlNode) element).getStringValue();
		}

		throw new NotImplementedException("element: " + element);
	}

	@Override
	public String getAttributeStringValue(@Nullable final Object attr) {

		if (log.isDebugEnabled()) {
			log.debugf("getAttributeStringValue(", attr, ")");
		}

		if (YamlNode.class.isInstance(attr)) {

			return ((YamlNode) attr).getStringValue();
		}

		throw new NotImplementedException();
	}

	/**
	 * return <tt>null</tt>.
	 */
	@Override
	public String getNamespaceStringValue(@Nullable final Object ns) {

		if (log.isDebugEnabled()) {
			log.debugf("getNamespaceStringValue(", ns, ")");
		}

		return null;
	}

	@Override
	public String getTextStringValue(final Object text) {

		if (log.isDebugEnabled()) {
			log.debugf("getTextStringValue(", text, ")");
		}

		nonNullArgument(text, "text");

		if (YamlNode.class.isInstance(text)) {

			return ((YamlNode) text).getStringValue();
		}

		if (isSimpleValue(text)) {

			return text.toString();
		}

		throw new NotImplementedException("text: " + text + " (class: "
				+ text.getClass() + ")");
	}

	/**
	 * return <tt>null</tt>.
	 */
	@Override
	public String getNamespacePrefix(@Nullable final Object ns) {

		if (log.isDebugEnabled()) {
			log.debugf("getNamespacePrefix(", ns, ")");
		}

		return null;
	}

	@Override
	public XPath parseXPath(final String xpathExpr) throws SAXPathException {

		final XPath xpath = new YamlXPath(rootNode, xpathExpr, nsContext, null,
				configuration);

		return xpath;
	}

	@Override
	public Iterator<?> getChildAxisIterator(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getChildAxisIterator(", node, ")");
		}

		nonNullArgument(node, "node");

		final List<YamlNode> list;

//		System.out.println("node.type:" + node.getClass() + ":" + node);
		if (YamlNode.class.isInstance(node)) {

			if (log.isDebugEnabled()) {
				log.debugf("  => getChildAxisIterator(): instanceof YamlNode");
			}

			final YamlNode[] children = ((YamlNode) node).getChildren();

			if (children == null) {

				if (log.isDebugEnabled()) {
					log.debugf("  => getChildAxisIterator(): ", null);
				}

				return null;
			}

			// throw new RuntimeException("oups");

			list = Arrays.asList(children);

		} else {

			final String[] names = getPropertyNames(node);

			if (log.isDebugEnabled()) {
				log.debugf("  => getChildAxisIterator(): names: ", names);
			}

			list = new ArrayList<YamlNode>();

			for (int i = 0; i < names.length; ++i) {

				final String name = names[i];

				final Object content = getObjectProperty(node, name);

				if (!configuration.isNodesElementsEverywhere()
						&& isSimpleValue(content)) {

					continue;
				}

				if (List.class.isInstance(content)) {

					for (final Object item : (List<?>) content) {

						final YamlNode child = new YamlNode(node, name, item,
								configuration);

						list.add(child);
					}

				} else {

					final YamlNode child = new YamlNode(node, name, content,
							configuration);

					list.add(child);
				}
			}
		}

		if (log.isDebugEnabled()) {
			log.debugf("  => getChildAxisIterator(): ", list);
		}

		return list.iterator();
	}

	@Override
	public Iterator<?> getDescendantAxisIterator(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getDescendantAxisIterator(", node, ")");
		}

		nonNullArgument(node, "node");

		final List<Object> descendants = new ArrayList<Object>();

		addDescendants(descendants, getChildAxisIterator(node));

		return descendants.iterator();
	}

	private void addDescendants(final List<Object> descendants,
			final Iterator<?> it) throws UnsupportedAxisException {

		nonNullArgument(descendants, "descendants");
		nonNullArgument(it, "iterator");

		while (it.hasNext()) {

			final Object descendant = it.next();

			descendants.add(descendant);

			addDescendants(descendants, getChildAxisIterator(descendant));
		}
	}

	@Override
	public Iterator<?> getParentAxisIterator(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getParentAxisIterator(", node, ")");
		}

		nonNullArgument(node, "node");

		throw new NotImplementedException();
	}

	@Override
	public Iterator<?> getAncestorAxisIterator(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getAncestorAxisIterator(", node, ")");
		}

		nonNullArgument(node, "node");

		throw new NotImplementedException();
	}

	@Override
	public Iterator<?> getFollowingSiblingAxisIterator(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getFollowingSiblingAxisIterator(", node, ")");
		}

		nonNullArgument(node, "node");

		if (YamlNode.class.isInstance(node)) {

			final Object parent = ((YamlNode) node).getParent();

			if (log.isDebugEnabled()) {
				log.debugf("getFollowingSiblingAxisIterator.parent: ", parent);
			}

			if (YamlNode.class.isInstance(parent)) {

				boolean started = false;

				final List<Object> list = new ArrayList<Object>();

				for (final Object child : ((YamlNode) parent).getChildren()) {

					if (log.isDebugEnabled()) {
						log.debugf(
								"getFollowingSiblingAxisIterator.parent.child: ",
								child);
					}

					if (started) {

						// log.debug("Adding.");

						list.add(child);

					} else if (child == node) {

						started = true;
					}
				}

				if (log.isDebugEnabled()) {
					log.debugf("  => getFollowingSiblingAxisIterator(): ", list);
				}

				return list.iterator();
			}

			System.err.println("node.parent.type: "
					+ parent.getClass().getName());
		}

		System.err.println("node.type: " + node.getClass().getName());

		throw new NotImplementedException();
	}

	@Override
	public Iterator<?> getPrecedingSiblingAxisIterator(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getPrecedingSiblingAxisIterator(", node, ")");
		}

		nonNullArgument(node, "node");

		throw new NotImplementedException();
	}

	@Override
	public Iterator<?> getFollowingAxisIterator(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getFollowingAxisIterator(", node, ")");
		}

		nonNullArgument(node, "node");

		throw new NotImplementedException();
	}

	@Override
	public Iterator<?> getPrecedingAxisIterator(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getPrecedingAxisIterator(", node, ")");
		}

		nonNullArgument(node, "node");

		throw new NotImplementedException();
	}

	@Override
	public Iterator<?> getAttributeAxisIterator(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getAttributeAxisIterator(", node, ")");
		}

		nonNullArgument(node, "node");

		if (YamlNode.class.isInstance(node)) {

			final YamlNode yamlNode = (YamlNode) node;

			final Object content = yamlNode.getContent();

			if (isSimpleValue(content)) {

				return Iterators.emptyIterator();
			}

			if (Map.class.isInstance(content)) {

				final String[] names = getPropertyNames(yamlNode);

				final List<YamlNode> children = new ArrayList<YamlNode>();

				for (int i = 0; i < names.length; ++i) {

					final String name = names[i];

					final Object value = getObjectProperty(yamlNode, name);

					if (isEmptyAttributeValue(value)) {

						if (configuration.isNodesEmptyAttributes()) {

							final YamlNode child = new YamlNode(yamlNode, name,
									"");

							children.add(child);
						}

					} else if (isSimpleValue(value)) {

						final YamlNode child = new YamlNode(yamlNode, name,
								value);

						children.add(child);
					}
				}

				if (log.isDebugEnabled()) {
					log.debugf("  => getAttributeAxisIterator(): ", children);
				}

				return children.iterator();
			}
		}

		throw new NotImplementedException("node: " + node);
	}

	@Override
	public Iterator<?> getNamespaceAxisIterator(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getNamespaceAxisIterator(", node, ")");
		}

		nonNullArgument(node, "node");

		throw new NotImplementedException();
	}

	@Override
	public Iterator<?> getSelfAxisIterator(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getSelfAxisIterator(", node, ")");
		}

		nonNullArgument(node, "node");

		return Iterators.singletonIterator(node);
	}

	@Override
	public Iterator<?> getDescendantOrSelfAxisIterator(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getDescendantOrSelfAxisIterator(", node, ")");
		}

		nonNullArgument(node, "node");

		return Iterators.concat(getSelfAxisIterator(node),
				getDescendantAxisIterator(node));
	}

	@Override
	public Iterator<?> getAncestorOrSelfAxisIterator(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getAncestorOrSelfAxisIterator(", node, ")");
		}

		nonNullArgument(node, "node");

		throw new NotImplementedException();
	}

	@Override
	public Object getDocument(final String uri) throws FunctionCallException {

		if (log.isDebugEnabled()) {
			log.debugf("getDocument(", uri, ")");
		}

		nonNullArgument(uri, "uri");

		throw new NotImplementedException();
	}

	@Override
	public Object getDocumentNode(@Nullable final Object node) {

		if (log.isDebugEnabled()) {
			log.debugf("getDocumentNode(", node, ")");
		}

		return rootNode;
	}

	@Override
	public Object getParentNode(final Object node)
			throws UnsupportedAxisException {

		if (log.isDebugEnabled()) {
			log.debugf("getParentNode(", node, ")");
		}

		if (node == rootNode) {

			return null;
		}

		nonNullArgument(node, "node");

		if (YamlNode.class.isInstance(node)) {

			return (((YamlNode) node).getParent());
		}

		throw new NotImplementedException("node: " + node);
	}

	@Override
	public String getProcessingInstructionTarget(@Nullable final Object pi) {

		if (log.isDebugEnabled()) {
			log.debugf("getProcessingInstructionTarget(", pi, ")");
		}

		throw new NotImplementedException();
	}

	@Override
	public String getProcessingInstructionData(@Nullable final Object pi) {

		if (log.isDebugEnabled()) {
			log.debugf("getProcessingInstructionData(", pi, ")");
		}

		throw new NotImplementedException();
	}

	@Override
	public String translateNamespacePrefixToUri(@Nullable final String prefix,
			@Nullable final Object element) {

		if (log.isDebugEnabled()) {
			log.debugf("translateNamespacePrefixToUri(", prefix, ", ", element,
					")");
		}

		throw new NotImplementedException();
	}

	@Override
	public Object getElementById(@Nullable final Object node,
			final String elementId) {

		if (log.isDebugEnabled()) {
			log.debugf("getElementById(", node, ", ", elementId, ")");
		}

		nonNullArgument(elementId, "elementId");

		throw new NotImplementedException();
	}

	@Override
	public short getNodeType(final Object node) {

		if (log.isDebugEnabled()) {
			log.debugf("getNodeType(", node, ")");
		}

		nonNullArgument(node, "node");

		throw new NotImplementedException();
	}

	private static String[] getPropertyNames(final Object node) {

		nonNullArgument(node, "node");

		if (node instanceof YamlWithProperties) {

			return ((YamlWithProperties) node).getPropertyNames();
		}

		final String[] propertyNames = YamlUtils.getPropertyNames(node);

		return propertyNames;

	}

	private static Object getObjectProperty(final Object node, final String name) {

		nonNullArgument(node, "node");
		nonNullArgument(name, "name");

		if (node instanceof YamlWithProperties) {

			return ((YamlWithProperties) node).getObjectProperty(name);
		}

		return YamlUtils.getObjectProperty(node, name);

	}
}
